package system.ui;

import java.util.LinkedList;
import java.util.Queue;
import system.communication.HandControlMessageNotifier;
import system.communication.HandControlPoller;
import system.communication.HandController;
import system.communication.Message;
import system.communication.MessageArg;
import system.communication.Monitor;
import system.track.Direction;
import system.track.SwitchAngle;
import system.track.Track;
import system.train.HandControlTrain;
import system.train.Train;

/**
 *
 * @author David
 */
public class Engine
{
    private static Engine instance;

    public static Engine getInstance()
    {
        if (instance == null)
        {
            instance = new Engine();
        }
        return instance;
    }

    private LinkedList<Train> trains;
    private Track track;

    private Gui gui;
    private Queue<String> messagesToPost;
    private int handControlState;
    private HandControlPoller hcp;

    private Engine()
    {
        /* Ensure the monitor has already been created. */
        Monitor.getInstance();
        messagesToPost = new LinkedList<String>();
        trains = new LinkedList<Train>();
        handControlState = 0;
    }

    /**
     * Build the track.
     *
     * This is a hack.  If we intialize the track during construction, the
     * calls to the monitor will cause the thread to wait for the message back.
     *
     * The message listener thread will then attempt to post the *m{ok}
     * back to the engine.
     *
     * Because we have not finished construction in the main thread, the
     * thread starts to build another engine because the static field is still
     * null.  The listener thread then deadlocks because it starts to
     * transmit messages and waiting for itself to poll the socket,
     * which it can't do since it's inside of Object#wait() in the
     * Monitor object.
     *
     * As a solution,we will build a track manually in the EvilOverLord,
     * after we've ensured a single call to Engine#getInstance() has
     * forced construction of all the underlying objects.
     *
     * Also, calling init after the gui has been set allows us to
     * post updates from the track to the GUI.
     */
    public void init()
    {
        track = new Track();
        forceGuiSyncWithTrains();
        gui.updateModeSelection('c');
        setUpCRequirements();
    }

    /**
     * Passes in to the GUI the engine to allow for
     * messages to be passed back into the GUI and state
     * management.
     *
     * Any messages recieved from the Monitor are queued until
     * this method is called.  They are the sent to the gui.
     *
     * @param gui the gui to post messages to and manipulate.
     */
    public void setGui(Gui gui)
    {
        this.gui = gui;
        for (String message : messagesToPost)
        {
            postMessage(message);
        }
    }

    /**
     * This function forces the engine to update all of the trains
     * statuses with thu GUI.
     */
    public void forceGuiSyncWithTrains()
    {
        for (Train train : trains)
        {
            postHandController(train.getID(), train.isHandControlled());
            postTrainDirection(train.getID(), train.getDirection());
            postTrainReservations(train.getID(), train.getReservedBlocks());
        }
    }

    /**
     * Sets up the C requirements, one train on
     * a normal corse.
     *
     * @throws NullPointerException if setGui is not called
     * with an appropriate Gui reference.
     */
    public void setUpCRequirements()
    {
        stopTrains();
        Train train = new Train(track.getStartPoint(1), 1);
        train.setName("Train 1: Automated");
        train.setRoute(system.track.Direction.CLOCKWISE);
        train.setDirection(Direction.CLOCKWISE);
        train.setSpeed(gui.getTrainSpeed(train.getID()));
        trains.add(train);
        gui.wipeTrainStatus(2);
        forceGuiSyncWithTrains();
        postMessage("Setup for one train, predermined route.");
    }

    public void setUpBRequirements()
    {
        stopTrains();
        setHandControllerOn(1);
        hcp = new HandControlPoller();
        hcp.setName("Hand Control Poller");
        hcp.setPriority(Thread.NORM_PRIORITY - 2);
        Monitor.getInstance().getHandControlStatus(1);
        HandControlTrain train = new
                HandControlTrain(track.getStartPoint(1), 1, 1);
        train.setName("Hand Controlled Train");
        gui.wipeTrainStatus(2);
        forceGuiSyncWithTrains();
        trains.add(train);
        postMessage("Setup for one train, hand controlled.");
    }

    /**
     * Starts the train threads.
     */
    public void runTrains()
    {
        for (Train train : trains)
        {
            train.start();
        }
        if (hcp != null)
        {
            hcp.start();
        }
    }

    /**
     * Kills the train threads.
     */
    public void stopTrains()
    {
        for (Train train : trains)
        {
            train.kill();
        }
        trains.clear();
        //Monitor.getInstance().stop();
        if (hcp != null)
        {
            hcp.end();
            hcp = null;
        }
    }

    /**
     * Sets the speed of the train to a specified amount.
     *
     * The return value allows us to keep the range check in one place and
     * where it makes sense to make the change, in the Train object.
     *
     * @param id the ID of the train to adjust
     * @param speed the speed to set it too.
     * @return the speed of the train after the set operation.
     */
    public int changeTrainSpeed(int id, int speed)
    {
        int returnVal = speed;
        for(Train train : trains)
        {
            if(train.getID() == id)
            {
                train.setSpeed(speed);
                returnVal = train.getSpeed();
            }
        }
        return returnVal;
    }

    /**
     * Same result as postMessage(String) but preprocceses a Message
     * object into a string.
     * @param message a message object to post to the gui.
     */
    public synchronized void postMessage(Message message, boolean sent)
    {
        String messageStr = processMessage(message, sent);
        postMessage(messageStr);
    }

    /**
     * Posts a string to the gui's message area.
     * @param messageStr the message to post.
     */
    public synchronized void postMessage(String messageStr)
    {
        try
        {
            gui.mAreaPrintln(messageStr);
        }
        catch (NullPointerException e)
        {
            messagesToPost.offer(messageStr);
        }
    }

    /**
     * Shuts down any background threads.
     */
    public void shutdown()
    {
        stopTrains();
        Monitor.getInstance().shutdown();
    }

    /**
     * Activates a hand controller of the specified number.
     *
     * @param id the id of the hand control to activate
     */
    public void setHandControllerOn(int id)
    {
        if (id == 1)
        {
            if(handControlState == 0 || handControlState == 2)
            {
                handControlState +=1;
            }
        } else if (id == 2) {
            if(handControlState == 0 || handControlState == 1)
            {
                handControlState += 2;
            }
        }
        //TODO Notify something in the model of the change.
    }

    /**
     * Deactivates a hand controller of the specified number.
     *
     * @param id the id of the hand control to deactivate
     */
    public void setHandControllerOff(int id)
    {
        if (id == 1)
        {
            if(handControlState == 1 || handControlState == 3)
            {
                handControlState -=1;
            }
        } else if (id == 2) {
            if(handControlState == 2 || handControlState == 3)
            {
                handControlState -= 2;
            }
        }
        //TODO Notify something in the model of the change.
    }

    /**
     * Returns the current hand controller state.
     *
     * The state is expressed as follows:
     * <table border="1">
     * <tr>
     * <td><strong>Value</strong></td>
     * <td><strong>Controler 1 Status</strong></td>
     * <td><strong>Controller 2 Status</strong></td>
     * </tr>
     * <tr>
     * <td><code>0</code></td>
     * <td>OFF</td>
     * <td>OFF</td>
     * </tr>
     * <tr>
     * <td><code>1</code></td>
     * <td>ON</td>
     * <td>OFF</td>
     * </tr>
     * <tr>
     * <td><code>2</code></td>
     * <td>OFF</td>
     * <td>ON</td>
     * </tr>
     * <tr>
     * <td><code>3</code></td>
     * <td>ON</td>
     * <td>ON</td>
     * </tr>
     * </table>
     * @return the on/off state of the hand controllers as specified above.
     */
    public int getHandControlState()
    {
        return handControlState;
    }

    /**
     * Sends an update on the direction of a train to the gui.
     *
     * @param id the ID of the train to update.
     * @param d the direction to send in.
     */
    public synchronized void postTrainDirection(int id, Direction d)
    {
        try
        {
            gui.updateTrainDirection(id, d.toString());
        }
        catch (NullPointerException e)
        {

        }
    }

    /**
     * Sends an update on whether a train is hand controlled to the gui.
     * @param id the id of the train to update.
     * @param handControlled <code>true</code> if the train is under the control
     * of a hand controller.<br /><code>false</code> if the train is not hand 
     * controlled.
     */
    public synchronized void postHandController(int id, boolean handControlled)
    {
        try
        {
            if(handControlled)
            {
                gui.updateHandController(id, "ON");
            }
            else
            {
                gui.updateHandController(id, "OFF");
            }
        }
        catch (NullPointerException e)
        {

        }
    }

    public synchronized void postTrainReservations(int id, int[] blocks)
    {
        String updateStr ="";
        for (int i : blocks)
        {
            updateStr += i + "  ";
        }
        try
        {
            gui.updateBlocksControlled(id, updateStr);
        }
        catch (NullPointerException e)
        {

        }
    }

    /**
     * Posts a change in the reserved status to the gui.
     * @param id the id of the block that's changed.
     * @param reserved <code>true</code> if the block is reserved,
     * <code>false</code> if the block is unreserved.
     */
    public synchronized void postBlockReservation(int id, boolean reserved)
    {
        try
        {
            gui.updateBlockReserved(id,
                    reserved ? "Yes" : "No");
        }
        catch (NullPointerException e)
        {

        }
    }

    /**
     * Posts a change in the direction of a block to the GUI.
     * @param id the block that changed direction
     * @param d the new direction
     */
    public synchronized void postBlockDirection(int id, Direction d)
    {
        try
        {
            gui.updateBlockDirection(id, d.toString());
        }
        catch (NullPointerException e)
        {

        }
    }

    /**
     * Posts a change in block speed to the gui.
     * @param id the id of the block changing speed
     * @param speed the new speed of the block
     */
    public synchronized void postBlockSpeed(int id, int speed)
    {
        try
        {
            gui.updateBlockSpeed(id, Integer.toString(speed));
        }
        catch (NullPointerException e)
        {

        }
    }

    /**
     * Posts a change in switch state to the gui.
     * @param id the id of the switch changing
     * @param a the new angle
     */
    public synchronized void postSwitchAngle(int id, SwitchAngle a)
    {
        try
        {
            gui.updateSwitch(id, a.toString());
        }
        catch (NullPointerException e)
        {

        }
    }

    /**
     * Posts a change in sensor tripped state to the gui.
     * @param id the id of the tripped sensor.
     */
    public synchronized void postTripSensor(int id)
    {
        try
        {
            gui.updateSensor(id, true);
        }
        catch (NullPointerException e)
        {

        }
    }

    private String processMessage(Message message, boolean sent)
    {
        String processedMessage = sent ? "Message sent:" :
            "Message Recieved: \n";
        processedMessage += "\tType: " + message.getType().toString() + "\n";
        processedMessage += "\tArguments:\n";
        for(MessageArg arg : message.getAvailableArgs())
        {
            processedMessage += arg.toString() + ": " +
                    message.getArg(arg) + "\n";
        }
        processedMessage += "***End Message***\n";
        return processedMessage;
    }
}
